/**
 * Copyright 2010 IWAMURO Motonori <vmi@nifty.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jp.vmi.servlet.filter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class JsessionidFilter implements Filter {

    public static final String MDC_KEY = "SID";

    private static final String DEFAULT_SESSION_CHECKER_KEY = "__CHECK_SESSION__";

    private static final String DEFAULT_SESSION_DISABLED_HTML = "session-disabled.html";

    private static final String NO_SESSION_ID = "-";

    private static final String NO_GET_SESSION = "*";

    private static final Logger log = LoggerFactory.getLogger(JsessionidFilter.class);

    private String sessionCheckerKey;

    private Pattern ignoreUrlPattern = null;

    private byte[] sessionDisabledHtml;

    public void init(FilterConfig config) throws ServletException {
        sessionCheckerKey = config.getInitParameter("session-checker-key");
        if (sessionCheckerKey != null) {
            log.info("session checker key: {}", sessionCheckerKey);
        } else {
            sessionCheckerKey = DEFAULT_SESSION_CHECKER_KEY;
            log.info("session checker key (default): {}", sessionCheckerKey);
        }

        String ignoreUrlPatternString = config.getInitParameter("ignore-url-pattern");
        if (ignoreUrlPatternString != null) {
            ignoreUrlPattern = Pattern.compile(ignoreUrlPatternString);
            log.info("ignore url pattern: {}", ignoreUrlPatternString);
        } else {
            log.info("ignore url pattern is empty. (default)");
        }

        String sessionDisabledHtmlName = config.getInitParameter("session-disabled-html");
        if (sessionDisabledHtmlName != null) {
            log.info("session disabled HTML: {}", sessionDisabledHtmlName);
        } else {
            sessionDisabledHtmlName = DEFAULT_SESSION_DISABLED_HTML;
            log.info("session disabled HTML (default): {}", sessionDisabledHtmlName);
        }
        InputStream is = JsessionidFilter.class.getResourceAsStream(sessionDisabledHtmlName);
        if (is == null)
            throw new ServletException("Can't open resource " + sessionDisabledHtmlName);
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            int c;
            while ((c = is.read()) != -1)
                os.write(c);
            os.close();
            sessionDisabledHtml = os.toByteArray();
        } catch (IOException e) {
            throw new ServletException(e);
        } finally {
            try {
                is.close();
            } catch (Exception e) {
                // ignore exception
            }
        }
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest sReq, ServletResponse sRes, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest req = (HttpServletRequest) sReq;
        final HttpServletResponse res = (HttpServletResponse) sRes;
        long start = System.nanoTime();

        String method = req.getMethod();
        try {
            if ("GET".equals(method)) {
                String uri = req.getRequestURI();
                if (ignoreUrlPattern == null || !ignoreUrlPattern.matcher(uri).find()) {
                    HttpSession session = req.getSession(false);
                    String sessionChecker = req.getParameter(sessionCheckerKey);
                    if (session != null) {
                        MDC.put(MDC_KEY, session.getId());
                        if (sessionChecker != null) {
                            if ("/.".equals(uri)) { // fix for wicket
                                uri = "/";
                                log.debug("URL fix for Wicket: /. -> /");
                            }
                            String location = res.encodeRedirectURL(uri);
                            if (log.isDebugEnabled())
                                log.debug("Strip session checker: {}", location);
                            res.sendRedirect(location);
                            return;
                        }
                    } else { // no session
                        Map<?, ?> parameterMap = req.getParameterMap();
                        if (parameterMap.isEmpty()) {
                            req.getSession(true);
                            String location = res.encodeRedirectURL(uri) + "?" + sessionCheckerKey + "=true";
                            if (log.isDebugEnabled())
                                log.debug("Add session checker: {}", location);
                            res.sendRedirect(location);
                        } else if (sessionChecker != null) { // can't get session after creating session.
                            res.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            res.setCharacterEncoding("UTF-8");
                            res.setContentType("text/html");
                            ServletOutputStream os = res.getOutputStream();
                            os.write(sessionDisabledHtml);
                            try {
                                os.close();
                            } catch (IOException e) {
                                // no operation
                            }
                            log.info("Can't use session: UA=[{}]", req.getHeader("User-Agent"));
                        }
                        return;
                    }
                } else {
                    MDC.put(MDC_KEY, NO_GET_SESSION);
                }
            } else if ("POST".equals(method)) {
                HttpSession session = req.getSession(false);
                MDC.put(MDC_KEY, session != null ? session.getId() : NO_SESSION_ID);
            } else {
                MDC.put(MDC_KEY, NO_GET_SESSION);
            }
            chain.doFilter(req, res);
        } finally {
            if (log.isDebugEnabled()) {
                double delta = ((double) System.nanoTime() - start) / 1000000;
                Object[] args = { String.format("%.1f", delta), method, req.getRequestURI() };
                log.debug("Finished: [{}ms] {} {}", args);
            }
            MDC.remove(MDC_KEY);
        }
    }
}
